pr_10_sales_funnel_analysis¶

Analysis of the mobile app sales funnel based on the results of the A/B test.

Анализ воронки продаж мобильного приложения¶

Проясним, как ведут себя пользователи мобильного приложения по продаже продуктов питания и выберем лучший шрифт.

Ключевые задачи:

  • Изучим воронку продаж:
    • Узнаем, как пользователи доходят до покупки;
    • Посчитаем количество пользователей, которые доходят до покупки;
    • Количество тех, кто «застревает» на предыдущих шагах. Поймем на каких именно;
  • Исследуем результаты A/A/B-эксперимента, посвященного выбору лучшего шрифта.

План работ:

Шаг 1. Получим данные

Откроем файл с данными и изучим общую информацию.

Шаг 2. Подготовим данные

  • Исправим названия столбцов;
  • Проверим пропуски и типы данных;
  • Добавим столбец даты и времени, а также отдельный столбец дат;

Шаг 3. Изучим и проверим данные

  • Вычислим:
    • Количество событий в логе;
    • Количества пользователей в логе;
    • Среднее количество событий на пользователя;
  • Поймем, данными за какой период мы располагаем?
    • Как меняется количество данных в зависимости от времени в разрезе групп.
    • Можно ли быть уверенным, что у нас одинаково полные данные за весь период?
    • Есть ли добавленные данные прошлых периодов?
    • Определим, с какого момента данные полные и отбросим более старые.
    • Данными за какой период мы располагаем на самом деле?
    • Много ли событий и пользователей мы потеряли, отбросив старые данные?
  • Проверим, что у нас есть пользователи из всех трёх экспериментальных групп.

Шаг 4. Изучим воронку событий

  • Посмотрим, какие события есть в логах, как часто они встречаются;
  • Посчитаем, сколько пользователей совершали каждое из этих событий;
  • Посчитаем долю пользователей, которые хоть раз совершали событие;
  • Проясним, в каком порядке происходят события и какие из них нужно учитывать в воронке приложения;
  • Посчитаем конверсию количества пользователей между этапами воронки приложения;
  • Поймем, на каком шаге теряем больше всего пользователей;
  • Вычислим, какая доля пользователей доходит от первого события до оплаты.

Шаг 5. Изучим результаты эксперимента

  • Посчитаем количество пользователей в каждой экспериментальной группе и объединенной контрольной группе;
  • Выберем уровень статистической значимости для сравнения четырех гипотез с учетом поправки Бонферрони;
  • Контрольные группы:
    • Проверим наличие двух контрольных групп, чтобы проверить корректность всех механизмов и расчётов;
    • Проверим, находят ли статистические критерии разницу между выборками контрольных групп;
    • Самое популярное событие:
      • Выберем самое популярное событие;
      • Посчитаем число пользователей, совершивших это событие в каждой из контрольных групп;
      • Посчитаем долю пользователей, совершивших это событие;
      • Проверим, будет ли различие между группами статистически достоверным. Для расчетов создадим функцию.
    • Прочие события воронки:
      • Посчитаем число пользователей, совершивших это событие в каждой из контрольных групп;
      • Посчитаем долю пользователей, совершивших это событие;
      • Проверим, будет ли различие между группами статистически достоверным;
    • Сделаем вывод о том, работает ли разбиение на группы корректно;
  • Аналогично поступим с группой с изменённым шрифтом.
    • Сравним группу B с каждой из контрольных групп, а также с объединенной контрольной группой.
    • Сравнение выполним по каждому из этапов воронки.
    • Выясним, будет ли различие между группами статистически достоверным.
  • Сделаем выводы из эксперимента.

Шаг 1. Получим данные¶

Подключим библиотеки.

In [1]:
import pandas as pd
import matplotlib.pyplot as plt
import scipy.stats as st
import datetime as dt
import numpy as np
import math as mth
import plotly.express as px

Откроем файл с данными и изучим общую информацию.

In [2]:
try:
    df0 = pd.read_csv('datasets/logs_exp.csv', sep='\t')
except:
    print('Файл не загружен. Проверьте путь.')
In [3]:
df0.head(3)
Out[3]:
EventName DeviceIDHash EventTimestamp ExpId
0 MainScreenAppear 4575588528974610257 1564029816 246
1 MainScreenAppear 7416695313311560658 1564053102 246
2 PaymentScreenSuccessful 3518123091307005509 1564054127 248

Описание данных¶

Каждая запись в логе — это действие пользователя, или событие.

  • EventName — название события;
  • DeviceIDHash — уникальный идентификатор пользователя;
  • EventTimestamp — время события;
  • ExpId — номер эксперимента: 246 и 247 — контрольные группы, а 248 — экспериментальная.

Создадим копию датафрейма.

In [4]:
df = df0.copy()

Выведем общую информацию.

In [5]:
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 244126 entries, 0 to 244125
Data columns (total 4 columns):
 #   Column          Non-Null Count   Dtype 
---  ------          --------------   ----- 
 0   EventName       244126 non-null  object
 1   DeviceIDHash    244126 non-null  int64 
 2   EventTimestamp  244126 non-null  int64 
 3   ExpId           244126 non-null  int64 
dtypes: int64(3), object(1)
memory usage: 7.5+ MB

Всего 244 125 записей в 4 столбцах.

Шаг 2. Подготовим данные¶

Переименуем столбцы для удобства работы.

In [6]:
df.rename(columns={ 'EventName': 'event_name', 'DeviceIDHash': 'user_id', 
                    'EventTimestamp':'event_timestamp', 'ExpId':'exp_id'}, inplace=True)

Посчитаем количество пропусков.

In [7]:
df.isna().sum()
Out[7]:
event_name         0
user_id            0
event_timestamp    0
exp_id             0
dtype: int64

В данных отсутствуют пропуски.

Добавим столбец event_dt с датой и временем события и отдельный столбец event_date просто с датой.

In [8]:
df['event_dt'] = pd.to_datetime(df['event_timestamp'], unit='s')
df['event_date'] = pd.to_datetime(df['event_dt'].dt.date)

Проверим полученный результат.

In [9]:
df.head(3)
Out[9]:
event_name user_id event_timestamp exp_id event_dt event_date
0 MainScreenAppear 4575588528974610257 1564029816 246 2019-07-25 04:43:36 2019-07-25
1 MainScreenAppear 7416695313311560658 1564053102 246 2019-07-25 11:11:42 2019-07-25
2 PaymentScreenSuccessful 3518123091307005509 1564054127 248 2019-07-25 11:28:47 2019-07-25

Добавим столбец с удобным названием группы тестирования.

In [10]:
# Справочник названий групп
test_groups_names = {246: 'A1', 247: 'A2', 248: 'B'}
df['group'] = df['exp_id'].map(test_groups_names)
df.head()
Out[10]:
event_name user_id event_timestamp exp_id event_dt event_date group
0 MainScreenAppear 4575588528974610257 1564029816 246 2019-07-25 04:43:36 2019-07-25 A1
1 MainScreenAppear 7416695313311560658 1564053102 246 2019-07-25 11:11:42 2019-07-25 A1
2 PaymentScreenSuccessful 3518123091307005509 1564054127 248 2019-07-25 11:28:47 2019-07-25 B
3 CartScreenAppear 3518123091307005509 1564054127 248 2019-07-25 11:28:47 2019-07-25 B
4 PaymentScreenSuccessful 6217807653094995999 1564055322 248 2019-07-25 11:48:42 2019-07-25 B

Проверим полученные типы данных.

In [11]:
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 244126 entries, 0 to 244125
Data columns (total 7 columns):
 #   Column           Non-Null Count   Dtype         
---  ------           --------------   -----         
 0   event_name       244126 non-null  object        
 1   user_id          244126 non-null  int64         
 2   event_timestamp  244126 non-null  int64         
 3   exp_id           244126 non-null  int64         
 4   event_dt         244126 non-null  datetime64[ns]
 5   event_date       244126 non-null  datetime64[ns]
 6   group            244126 non-null  object        
dtypes: datetime64[ns](2), int64(3), object(2)
memory usage: 13.0+ MB

Проверим данные на наличие дубликатов.

In [12]:
df.duplicated().sum()
Out[12]:
413

Обнаружено 413 дубликатов. Удалим их.

In [13]:
df.drop_duplicates(inplace=True)

Проверим результат.

In [14]:
df.duplicated().sum()
Out[14]:
0

Дубликаты удалены.

Проверим, что пользователи присутствуют только в одной из групп теста.

Пользователи группы А1.

In [15]:
group_a1_users_id = pd.DataFrame(df.query('group == "A1"')['user_id'].unique())
group_a1_users_id.columns=['user_id']
group_a1_users_id.head(3)
Out[15]:
user_id
0 4575588528974610257
1 7416695313311560658
2 8351860793733343758

Пользователи группы А2.

In [16]:
group_a2_users_id = pd.DataFrame(df.query('group == "A2"')['user_id'].unique())
group_a2_users_id.columns=['user_id']
group_a2_users_id.head(3)
Out[16]:
user_id
0 1850981295691852772
1 948465712512390382
2 2140904690380565988

Пользователи группы B.

In [17]:
group_b_users_id = pd.DataFrame(df.query('group == "B"')['user_id'].unique())
group_b_users_id.columns=['user_id']
group_b_users_id.head(3)
Out[17]:
user_id
0 3518123091307005509
1 6217807653094995999
2 2547684315586332355

Проверим пересечения идентификаторов пользователей.

In [18]:
group_a1_users_id.merge(group_a2_users_id, on='user_id')
Out[18]:
user_id
In [19]:
group_a1_users_id.merge(group_b_users_id, on='user_id')
Out[19]:
user_id
In [20]:
group_a2_users_id.merge(group_b_users_id, on='user_id')
Out[20]:
user_id

Все пользователи принадлежат только к одной из групп теста.

Вывод. Шаг 2. Подготовим данные¶

Данные подготовлены к работе. В данных отсутствовали пропуски.

  • Выполнено переименование столбцов;
  • Добавлен столбец с датой и временем события event_dt;
  • Добавлен столбец с датой события event_date;
  • Удалено 413 дубликатов.
  • Пересечений идентификаторов пользователей между группами теста не выявлено.

Шаг 3. Изучим и проверим данные¶

Количество событий в логе.

In [21]:
events_before_deletion = df.shape[0]
print('Количество событий в выборке:', events_before_deletion)
Количество событий в выборке: 243713

Количества пользователей в логе.

In [22]:
users_before_deletion = df['user_id'].nunique()
print('Количество пользователей в выборке:', users_before_deletion)
Количество пользователей в выборке: 7551

Среднее, минимальное, максимальное количество событий на пользователя.

In [23]:
events_per_user = df.groupby('user_id').agg({'event_name': 'count'})
print(  
    f'Среднее количество событий на пользователя: {events_per_user.event_name.mean():0.1f};\n'
    f'Минимальное количество событий на пользователя: {events_per_user.event_name.min()};\n',
    f'Максимальное количество событий на пользователя: {events_per_user.event_name.max()}.'
)
Среднее количество событий на пользователя: 32.3;
Минимальное количество событий на пользователя: 1;
 Максимальное количество событий на пользователя: 2307.

В среднем на пользователя приходится 32.3 события, максимум 2307. В данных присутствуют выбросы по количеству событий.

Период, за который мы располагаем данными.

Определим минимальную и максимальную даты.

In [24]:
df.pivot_table(index='group', values='event_date', aggfunc=['min', 'max'])
Out[24]:
min max
event_date event_date
group
A1 2019-07-25 2019-08-07
A2 2019-07-25 2019-08-07
B 2019-07-25 2019-08-07

Во всех группах тестирования представлены данные за период с 25 июля по 7 августа 2019 года.

Посчитаем количество записей о событиях в зависимости от даты и группы тестирования.

In [25]:
result = df.pivot_table(index='event_date', columns='group', values='user_id', aggfunc='count')
result
Out[25]:
group A1 A2 B
event_date
2019-07-25 4 1 4
2019-07-26 14 8 9
2019-07-27 24 23 8
2019-07-28 33 36 36
2019-07-29 55 58 71
2019-07-30 129 138 145
2019-07-31 620 664 746
2019-08-01 11561 12306 12274
2019-08-02 10946 10990 13618
2019-08-03 10575 11024 11683
2019-08-04 11514 9942 11512
2019-08-05 12368 10949 12741
2019-08-06 11726 11720 12342
2019-08-07 10612 10091 10393

31 июля количество событий существенно возрастает. С 1 августа количество событий увеличивается на два порядка.

Построим график количества событий по датам для каждой группы тестирования.

In [26]:
result.plot()
plt.ylabel('Количество событий')
plt.title('Количество событий по группам тестирования и датам')
plt.show()
No description has been provided for this image

С 1 по 6 августа событий в группе B стабильно больше, чем в контрольных группах.

In [27]:
ax, fig = plt.subplots(figsize=(10,5))
  
X_axis = np.arange(result.shape[0]) 
  
rects = plt.bar(X_axis - 0.2, result['A1'], 0.2, label = 'A1') 
plt.bar(X_axis, result['A2'], 0.2, label = 'A2') 
plt.bar(X_axis + 0.2, result['B'], 0.2, label = 'B') 

plt.xlabel('Даты') 
plt.ylabel('Количество событий') 
plt.title('Количество событий по группам') 
plt.legend() 
plt.show() 
No description has been provided for this image

В минимальном количестве события тестирования возникают с 25 июля 2019 года.
Вероятно, первая неделя была посвящена проверке работы инструментов для сбора статистики, подтверждения корректности разделения пользователей на группы.
С 1 августа началось активное привлечение трафика, количество событий возросло на два порядка.
Тест продлился до 7 августа 2019 года.
Количество событий по группам соизмеримо.

Технически в логи новых дней по некоторым пользователям могут «доезжать» события из прошлого — это может «перекашивать данные». Проверим данные на такой артефакт.

Посмотрим на события за 31 июля в разбивке по часам.

In [28]:
result = df.loc[df['event_date'] == "2019-07-31"].sort_values(by=['user_id', 'event_dt'])
result['hour'] = result['event_dt'].dt.round('1h')
result.head()
Out[28]:
event_name user_id event_timestamp exp_id event_dt event_date group hour
1908 MainScreenAppear 33176906322804559 1564601363 248 2019-07-31 19:29:23 2019-07-31 B 2019-07-31 19:00:00
1806 OffersScreenAppear 33589551945846495 1564597381 248 2019-07-31 18:23:01 2019-07-31 B 2019-07-31 18:00:00
1783 MainScreenAppear 38880205595577265 1564596510 248 2019-07-31 18:08:30 2019-07-31 B 2019-07-31 18:00:00
1418 MainScreenAppear 50002244844355989 1564584723 246 2019-07-31 14:52:03 2019-07-31 A1 2019-07-31 15:00:00
2584 MainScreenAppear 65356103704674532 1564611872 246 2019-07-31 22:24:32 2019-07-31 A1 2019-07-31 22:00:00

Посчитаем количество событий за каждый час.

In [29]:
result_agg = result.groupby('hour').agg({'user_id': 'count'})
result_agg.columns = ['events']
result_agg.reset_index(inplace=True)
result_agg['hour'] = pd.DatetimeIndex(result_agg['hour']).hour
result_agg.tail(7)
Out[29]:
hour events
17 18 96
18 19 92
19 20 55
20 21 258
21 22 393
22 23 178
23 0 33

Построим график количества событий по часам за 31 июля.

In [30]:
plt.bar(result_agg['hour'], result_agg['events'])
plt.title('Количество событий в час за 31 июля 2019 года')
plt.xlabel('Час')
plt.ylabel('Количество событий')
plt.show()
No description has been provided for this image

С 21 часа 31 июля 2019 года происходит существенное увеличение количества событий.
Видимо, до этого шла проверка работы теста. С 21:00 началось активное привлечение пользователей.

Уберем из выборки все события ранее 21:00 31 июля 2019 года.

In [31]:
df = df[df['event_dt'] >= '2019-07-31 21:00:00']
df.head(3)
Out[31]:
event_name user_id event_timestamp exp_id event_dt event_date group
1990 MainScreenAppear 7701922487875823903 1564606857 247 2019-07-31 21:00:57 2019-07-31 A2
1991 MainScreenAppear 2539077412200498909 1564606905 247 2019-07-31 21:01:45 2019-07-31 A2
1992 OffersScreenAppear 3286987355161301427 1564606941 248 2019-07-31 21:02:21 2019-07-31 B

Самое раннее событие в выборке.

In [32]:
df['event_dt'].min()
Out[32]:
Timestamp('2019-07-31 21:00:57')

Посчитаем количество событий после удаления данных.

In [33]:
events_after_deletion = df.shape[0]
print('Количество событий после удаления данных:', events_after_deletion, 
      '\nОтброшено', (events_before_deletion - events_after_deletion),
      'событий, что составляет ', '{:.1%}'.format(
          (events_before_deletion - events_after_deletion) / events_before_deletion), 'от общего числа.'
     )
Количество событий после удаления данных: 241724 
Отброшено 1989 событий, что составляет  0.8% от общего числа.

Посчитаем количество пользователей после удаления данных.

In [34]:
users_after_deletion = df['user_id'].nunique()
print('Количество пользователей после удаления данных:', users_after_deletion, 
      '\nОтброшено', (users_before_deletion - users_after_deletion),
      'пользователей, что составляет ', '{:.1%}'.format(
          (users_before_deletion - users_after_deletion) / users_before_deletion), 'от общего числа.'
     )
Количество пользователей после удаления данных: 7538 
Отброшено 13 пользователей, что составляет  0.2% от общего числа.

Теперь мы уверены, что доля мала.

Сопоставим количество пользователей в каждой из экспериментальных групп.

In [35]:
ratio = df.pivot_table(index='group', values='user_id', aggfunc='nunique')
ratio['ratio'] = ratio['user_id'] / ratio['user_id'].sum()
ratio.columns = ['users', 'ratio']
ratio.style.format({'ratio': '{0:.2%}'})
Out[35]:
  users ratio
group    
A1 2484 32.95%
A2 2517 33.39%
B 2537 33.66%

Всего пользователей в группе A1 - 2484 человека (32.95%), A2 - 2517 (33.39%), B - 2537 (33.66%).

За какой период мы располагаем данными на самом деле.

In [36]:
df.pivot_table(index='group', values='event_date', aggfunc=['min', 'max'])
Out[36]:
min max
event_date event_date
group
A1 2019-07-31 2019-08-07
A2 2019-07-31 2019-08-07
B 2019-07-31 2019-08-07

В каждой из трех экспериментальных групп пользователи есть. Количество сопоставимо.

Мы убедились, что пользователи сохранены по каждой из трех групп после фильтрации. Приступим к изучению воронки.

Вывод. Шаг 3. Изучим и проверим данные¶

Всего в логе 243 713 уникальных события от 7551 уникального пользователя.
В среднем на пользователя приходится 32.3 события, максимум 2307 шт.,
то есть в данных присутствуют выбросы по количеству событий.
Во всех группах тестирования представлены данные за период с 25 июля по 7 августа 2019 года.

В минимальном количестве события тестирования возникают с 25 июля 2019 года.
Первая неделя была посвящена проверке работы инструментов для сбора статистики, подтверждения корректности разделения пользователей на группы.
С 31 июля 21:00 началось активное привлечение трафика.
Тест продлился до 7 августа 2019 года.
Количество событий по группам соизмеримо.

Данные прошлых периодов мы удалили из выборки.

Удалено 1989 событий (0.8%) и 13 пользователей (0.2%).

Полными данные можно считать с 31 июля по 7 августа 2019 года.

Количество событий после удаления данных: 241724
Количество пользователей после удаления данных: 7538

Всего пользователей в группе A1 - 2484 человека (32.95%), A2 - 2517 (33.39%), B - 2537 (33.66%).
В каждой из трех экспериментальных групп пользователи есть. Количество сопоставимо.

Шаг 4. Воронка событий¶

События в логах

Посмотрим, какие события есть в логах, как часто они встречаются.

In [37]:
funnel = df.pivot_table(index='event_name', values='user_id', aggfunc={'count'})
funnel['ratio'] = funnel['count'] / funnel['count'].sum()
funnel.sort_values(by='ratio', ascending=False, inplace=True)
funnel.style.format({'ratio': '{0:.1%}'})
Out[37]:
  count ratio
event_name    
MainScreenAppear 117889 48.8%
OffersScreenAppear 46531 19.2%
CartScreenAppear 42343 17.5%
PaymentScreenSuccessful 33951 14.0%
Tutorial 1010 0.4%

В перечне событий видим:

  • MainScreenAppear (Появится главный экран) - 48.8% от общего числа событий в логах;
  • OffersScreenAppear (Появится экран предложений) - 19.2%;
  • CartScreenAppear (Появится экран корзины) - 17.5%;
  • PaymentScreenSuccessful (Экран оплаты прошел успешно) - 14.0%;
  • Tutorial (Руководство) - 0.4%.

Сколько пользователей совершали каждое из этих событий

In [38]:
funnel_users = df.pivot_table(index='event_name', values='user_id', aggfunc={'nunique'})
funnel_users['ratio'] = funnel_users['nunique'] / funnel_users['nunique'].sum()
funnel_users.sort_values(by='nunique', ascending=False, inplace=True)
funnel_users.style.format({'ratio': '{0:.1%}'})
Out[38]:
  nunique ratio
event_name    
MainScreenAppear 7423 36.9%
OffersScreenAppear 4597 22.8%
CartScreenAppear 3736 18.6%
PaymentScreenSuccessful 3540 17.6%
Tutorial 843 4.2%

Количество уникальных пользователей, хоть раз совершивших событие, и доля этих пользователей от общего числа:
MainScreenAppear (Появится Главный экран) - 7423 чел., 36.9%;
OffersScreenAppear (Появится экран предложений) - 4597 чел., 22.8%;
CartScreenAppear (Появится экран корзины) - 3736 чел., 18.6%;
PaymentScreenSuccessful (Экран оплаты прошел успешно) - 3540 чел., 17.6%;
Tutorial (Руководство) - 843 чел., 4.2%.

Порядок событий в воронке продаж

Все события, кроме Tutorial, относятся к воронке продаж. Tutorial - это вспомогательная активность и напрямую к воронке продаж отношения не имеет.

Корректное расположение событий в воронке:

  • MainScreenAppear (Появится Главный экран);
  • OffersScreenAppear (Появится экран предложений);
  • CartScreenAppear (Появится экран корзины);
  • PaymentScreenSuccessful (Экран оплаты прошел успешно).

Объявим список с перечнем этапов воронки.

In [39]:
funnel_names = ['MainScreenAppear', 'OffersScreenAppear', 'CartScreenAppear', 'PaymentScreenSuccessful']

Отобразим воронку событий на графике.

In [40]:
result = funnel[funnel.index != 'Tutorial'].reset_index()
fig = px.funnel(result, x='count', y='event_name')
fig.update_layout(
    autosize=False,
    width=800,
    height=350,
    )
fig.update_layout(title='Воронка событий', title_x = 0.5)
fig.show()

Отобразим воронку количества пользователей на графике.

In [41]:
result = funnel_users[funnel_users.index != 'Tutorial'].reset_index()
fig = px.funnel(result, x='nunique', y='event_name')
fig.update_layout(
    autosize=False,
    width=800,
    height=350,
    )
fig.update_layout(title='Воронка пользователей', title_x = 0.5)
fig.show()

Создадим столбец с номером этапа воронки продаж.

In [42]:
df['funnel'] = df['event_name'].map({'MainScreenAppear': 1, 
                                           'OffersScreenAppear': 2, 
                                           'CartScreenAppear': 3, 
                                           'PaymentScreenSuccessful': 4})
df.head(5)
Out[42]:
event_name user_id event_timestamp exp_id event_dt event_date group funnel
1990 MainScreenAppear 7701922487875823903 1564606857 247 2019-07-31 21:00:57 2019-07-31 A2 1.0
1991 MainScreenAppear 2539077412200498909 1564606905 247 2019-07-31 21:01:45 2019-07-31 A2 1.0
1992 OffersScreenAppear 3286987355161301427 1564606941 248 2019-07-31 21:02:21 2019-07-31 B 2.0
1993 OffersScreenAppear 3187166762535343300 1564606943 247 2019-07-31 21:02:23 2019-07-31 A2 2.0
1994 MainScreenAppear 1118952406011435924 1564607005 248 2019-07-31 21:03:25 2019-07-31 B 1.0

Посчитаем конверсию количества пользователей между этапами воронки продаж.

In [43]:
funnel_cr = df[df['funnel'].notna()].pivot_table(index=['funnel', 'event_name'], 
                                                       values='user_id', aggfunc='nunique')
funnel_cr.columns = ['users']
funnel_cr['cr_previous'] = funnel_cr['users'] / funnel_cr['users'].shift(1)
funnel_cr['cr_pay'] = funnel_cr['users'] / funnel_cr['users'].shift(3)
funnel_cr.style.format({'cr_previous': '{0:.1%}', 'cr_pay': '{0:.1%}'})
Out[43]:
    users cr_previous cr_pay
funnel event_name      
1.000000 MainScreenAppear 7423 nan% nan%
2.000000 OffersScreenAppear 4597 61.9% nan%
3.000000 CartScreenAppear 3736 81.3% nan%
4.000000 PaymentScreenSuccessful 3540 94.8% 47.7%

Конверсия пользователей:

  • с этапа воронки MainScreenAppear на OffersScreenAppear - 61.9%;
  • с этапа OffersScreenAppear в CartScreenAppear - 81.3%;
  • с этапа CartScreenAppear в PaymentScreenSuccessful - 94.8%.

Больше всего пользователей теряется при переходе с первого шага на второй (с главного экрана на экран с предложениями).
Потери составляют 38.1%.

До этапа оплаты доходит 47.7% уникальных пользователей, увидевших главный экран.

Проверим, все ли пользователи, которые есть на поздних этапах воронки, присутствуют на более ранних этапах.

In [44]:
for funnel_l in range(1,5):
    for funnel_r in range(funnel_l+1, 5):
        user_id_funnel_l = df.loc[df['funnel'] == funnel_l, 'user_id']
        user_id_funnel_r = df.loc[df['funnel'] == funnel_r, 'user_id']
        lost_users_cnt = user_id_funnel_r[~user_id_funnel_r.isin(user_id_funnel_l)].shape[0]
        print(funnel_l, funnel_r, 'Потерялись:', lost_users_cnt)
1 2 Потерялись: 950
1 3 Потерялись: 911
1 4 Потерялись: 747
2 3 Потерялись: 119
2 4 Потерялись: 16
3 4 Потерялись: 5

Есть пользователи, которые отсутствуют на ранних этапах воронки, но присутствуют на более поздних.
Пользователи, которые не прошли MainScreenAppear есть в OffersScreenAppear 950 человек, CartScreenAppear 911 человек, PaymentScreenSuccessful 747 человек.

Пользователи, которые не прошли OffersScreenAppear есть в CartScreenAppear 119 человек, PaymentScreenSuccessful 16 человек.

Пользователи, которые не прошли CartScreenAppear есть в PaymentScreenSuccessful 5 человек.

Таким образом, дизайн приложения позволяет выполнять покупки и оплаты, минуя главный экран и другие промежуточные экраны.

Вывод. Шаг 4. Воронка событий¶

В перечне событий видим:

  • MainScreenAppear (Появится главный экран) - 48.8% от общего числа событий в логах;
  • OffersScreenAppear (Появится экран предложений) - 19.2%;
  • CartScreenAppear (Появится экран корзины) - 17.5%;
  • PaymentScreenSuccessful (Экран оплаты прошел успешно) - 14.0%;
  • Tutorial (Руководство) - 0.4%.

Количество уникальных пользователей, хоть раз совершивших событие, и доля этих пользователей от общего числа:
MainScreenAppear (Появится Главный экран) - 7423 чел., 36.9%;
OffersScreenAppear (Появится экран предложений) - 4597 чел., 22.8%;
CartScreenAppear (Появится экран корзины) - 3736 чел., 18.6%;
PaymentScreenSuccessful (Экран оплаты прошел успешно) - 3540 чел., 17.6%;
Tutorial (Руководство) - 843 чел., 4.2%.

Все события, кроме Tutorial, относятся к воронке продаж. Tutorial - это вспомогательная активность и напрямую к воронке продаж отношения не имеет.

Корректное расположение событий в воронке:

  • MainScreenAppear (Появится Главный экран);
  • OffersScreenAppear (Появится экран предложений);
  • CartScreenAppear (Появится экран корзины);
  • PaymentScreenSuccessful (Экран оплаты прошел успешно).

Конверсия пользователей:

  • с этапа воронки MainScreenAppear на OffersScreenAppear - 61.9%;
  • с этапа OffersScreenAppear в CartScreenAppear - 81.3%;
  • с этапа CartScreenAppear в PaymentScreenSuccessful - 94.8%.

Больше всего пользователей теряется при переходе с первого шага на второй (с главного экрана на экран с предложениями).
Потери составляют 38.1%.

До этапа оплаты доходит 47.7% уникальных пользователей, увидевших главный экран.

Есть пользователи, которые отсутствуют на ранних этапах воронки, но присутствуют на более поздних.
Пользователи, которые не прошли MainScreenAppear есть в OffersScreenAppear 950 человек, CartScreenAppear 911 человек, PaymentScreenSuccessful 747 человек.

Пользователи, которые не прошли OffersScreenAppear есть в CartScreenAppear 119 человек, PaymentScreenSuccessful 16 человек.

Пользователи, которые не прошли CartScreenAppear есть в PaymentScreenSuccessful 5 человек.

Таким образом, дизайн приложения позволяет выполнять покупки и оплаты, минуя главный экран и другие промежуточные экраны.

Обнаруженная проблема — провал на первом шаге: от MainScreenAppear к OffersScreenAppear. Вероятно, нужно лучше прорабатывать механику приложения, чтобы пользователи переходили к OffersScreen.

Шаг 5. Результаты эксперимента¶

Уровень статистической значимости¶

Выберем уровень статистической значимости для сравнения четырех гипотез с учетом поправки Бонферрони.

Всего будут сравниваться четыре независимые выборки: A1 / A2 ( 246 / 247 ), A1 / B ( 246 / 248 ), A2 / B ( 247 / 248 ), (A1 + A2) / B ( 246 + 247 / 248 ). Для каждой пары будем сравнивать четыре этапа воронки. Будет применен один статистический критерий. Таким образом, имеем множественное сравнение выборки B с другими независимыми от B выборками.

Количество сравнений.

In [45]:
m = 4 * 4 * 1
m
Out[45]:
16

Применим поправку Бонферрони для уточнения критерия статистической значимости.

In [46]:
alpha = 0.05
alpha /= m
print('Принятый критерий статистической значимости с учетом корректировки Бонферрони:', alpha)
Принятый критерий статистической значимости с учетом корректировки Бонферрони: 0.003125

Подготовим данные для проверки гипотез¶

Создадим новый датафрейм, в который добавим данные по объединенной контрольной группе. Для этого отберем данные групп A1, A2 и переименуем название группы в значение A3.

In [47]:
df.shape[0]
Out[47]:
241724
In [48]:
df_u = df.query('group == "A1" or group == "A2"').copy()
df_u['group'] = 'A3'
df_u
Out[48]:
event_name user_id event_timestamp exp_id event_dt event_date group funnel
1990 MainScreenAppear 7701922487875823903 1564606857 247 2019-07-31 21:00:57 2019-07-31 A3 1.0
1991 MainScreenAppear 2539077412200498909 1564606905 247 2019-07-31 21:01:45 2019-07-31 A3 1.0
1993 OffersScreenAppear 3187166762535343300 1564606943 247 2019-07-31 21:02:23 2019-07-31 A3 2.0
1996 OffersScreenAppear 3511569580412335882 1564607172 246 2019-07-31 21:06:12 2019-07-31 A3 2.0
1997 OffersScreenAppear 3511569580412335882 1564607236 246 2019-07-31 21:07:16 2019-07-31 A3 2.0
... ... ... ... ... ... ... ... ...
244121 MainScreenAppear 4599628364049201812 1565212345 247 2019-08-07 21:12:25 2019-08-07 A3 1.0
244122 MainScreenAppear 5849806612437486590 1565212439 246 2019-08-07 21:13:59 2019-08-07 A3 1.0
244123 MainScreenAppear 5746969938801999050 1565212483 246 2019-08-07 21:14:43 2019-08-07 A3 1.0
244124 MainScreenAppear 5746969938801999050 1565212498 246 2019-08-07 21:14:58 2019-08-07 A3 1.0
244125 OffersScreenAppear 5746969938801999050 1565212517 246 2019-08-07 21:15:17 2019-08-07 A3 2.0

156849 rows × 8 columns

Объединим полученный датафрейм с исходным. Получим набор данных для групп контрольных A1, A2, объединенной контрольной A3 и группы с измененным шрифтом B.

In [49]:
df_u = pd.concat([df_u, df])
df_u
Out[49]:
event_name user_id event_timestamp exp_id event_dt event_date group funnel
1990 MainScreenAppear 7701922487875823903 1564606857 247 2019-07-31 21:00:57 2019-07-31 A3 1.0
1991 MainScreenAppear 2539077412200498909 1564606905 247 2019-07-31 21:01:45 2019-07-31 A3 1.0
1993 OffersScreenAppear 3187166762535343300 1564606943 247 2019-07-31 21:02:23 2019-07-31 A3 2.0
1996 OffersScreenAppear 3511569580412335882 1564607172 246 2019-07-31 21:06:12 2019-07-31 A3 2.0
1997 OffersScreenAppear 3511569580412335882 1564607236 246 2019-07-31 21:07:16 2019-07-31 A3 2.0
... ... ... ... ... ... ... ... ...
244121 MainScreenAppear 4599628364049201812 1565212345 247 2019-08-07 21:12:25 2019-08-07 A2 1.0
244122 MainScreenAppear 5849806612437486590 1565212439 246 2019-08-07 21:13:59 2019-08-07 A1 1.0
244123 MainScreenAppear 5746969938801999050 1565212483 246 2019-08-07 21:14:43 2019-08-07 A1 1.0
244124 MainScreenAppear 5746969938801999050 1565212498 246 2019-08-07 21:14:58 2019-08-07 A1 1.0
244125 OffersScreenAppear 5746969938801999050 1565212517 246 2019-08-07 21:15:17 2019-08-07 A1 2.0

398573 rows × 8 columns

Количество записей соответствует ожидаемому.

Посчитаем количество пользователей в каждой группе.

In [50]:
groups_users = df_u.pivot_table(index='group', values='user_id', aggfunc='nunique')
groups_users.columns = ['users']
groups_users
Out[50]:
users
group
A1 2484
A2 2517
A3 5001
B 2537

Объявим функцию для вывода количества пользователей в соответствующей группе тестирования.

In [51]:
def users_group_cnt(group='A1', data=df_u):
    return data.query('group == @group')['user_id'].nunique()
In [52]:
users_group_cnt(group='A1')
Out[52]:
2484
In [53]:
users_group_cnt(group='B')
Out[53]:
2537

Функция работает корректно.

Объявим функцию для подсчета количества пользователей на соответствующем этапе воронки для выбранной группы A/A/B теста.

In [54]:
def users_funnel_cnt(group='A1', funnel=1, data=df_u):
    return data.query('group == @group and funnel == @funnel')['user_id'].nunique()
In [55]:
users_funnel_cnt(group='A1', funnel=1)
Out[55]:
2450
In [56]:
users_funnel_cnt(group='A2', funnel=1)
Out[56]:
2479

Функция работает корректно.

Сравнение контрольных групп¶

Проверим, находят ли статистические критерии разницу между выборками A1 и A2.

Сравним доли пользователей, которые находятся на определенном этапе воронки продаж, относительно общего числа пользователей этой группы.

Доли пользователей первого этапа воронки для контрольных групп¶

Определим гипотезы:
Нулевая - Доли пользователей первого этапа воронки между контрольными группами A1 A2 равны.
Альтернативная - Доли пользователей первого этапа воронки между контрольными группами A1 A2 статистически значимо отличаются.

Проведем вычисления для первого этапа воронки для контрольных групп.

Объявим переменные для обозначения групп теста и этапа воронки продаж.

In [57]:
group_1 = 'A1'
group_2 = 'A2'
funnel = 1

Количество пользователей в группах.

In [58]:
users_group_cnt(group_1)
Out[58]:
2484
In [59]:
users_group_cnt(group_2)
Out[59]:
2517

Количество пользователей искомого этапа воронки.

In [60]:
users_funnel_cnt(group_1, funnel)
Out[60]:
2450
In [61]:
users_funnel_cnt(group_2, funnel)
Out[61]:
2479

Проверим статистическую значимость гипотезы о том, что конверсия обеих групп равна. Применим Z-критерий.

In [62]:
# Выводить ли промежуточные расчеты
diagnostic = False

print('Группы:', group_1, group_2)
print('Этап воронки:', funnel, ' - ', funnel_names[funnel - 1])

successes = np.array([users_funnel_cnt(group_1, funnel), users_funnel_cnt(group_2, funnel)])
trials = np.array([users_group_cnt(group_1), users_group_cnt(group_2)])

# пропорция успехов в первой группе:
p1 = successes[0]/trials[0]

# пропорция успехов во второй группе:
p2 = successes[1]/trials[1]

# пропорция успехов в комбинированном датасете:
p_combined = (successes[0] + successes[1]) / (trials[0] + trials[1])

# разница пропорций в датасетах
difference = p1 - p2 

print('Доля пользователей этапа воронки к числу пользователей в группе', p1, p2 )

if diagnostic: 
    # Выводим диагностические данные
    print('p_combined', p_combined)
    print('difference', difference)

if difference == 0:
    p_value = 1    
else:        
    # считаем статистику в стандартных отклонениях стандартного нормального распределения
    z_value = difference / mth.sqrt(p_combined * (1 - p_combined) * (1/trials[0] + 1/trials[1]))

    # задаем стандартное нормальное распределение (среднее 0, стандартное отклонение 1)
    distr = st.norm(0, 1)  

    p_value = (1 - distr.cdf(abs(z_value))) * 2    
        
    if diagnostic: 
        # Выводим диагностические данные
        print('z_value', z_value)
        print('distr.cdf(abs(z_value))', distr.cdf(abs(z_value)))    
    
print('p-значение: ', p_value)

if p_value < alpha:
    print('Отвергаем нулевую гипотезу: между долями есть значимая разница')
else:
    print(
        'Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными'
    ) 
Группы: A1 A2
Этап воронки: 1  -  MainScreenAppear
Доля пользователей этапа воронки к числу пользователей в группе 0.9863123993558777 0.9849026618990863
p-значение:  0.6756217702005545
Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными

Функции для сравнения выборок¶

Завернем проведенные вычисления в функцию и сравним контрольные выборки по другим этапам воронки продаж.

Функция для расчета Z-критерия.

In [63]:
def z_criterion(group_1 = 'A1', group_2 = 'A2', funnel = 2, diagnostic=False):    
    ''' group_1, group_2 - группы теста, сравниваемые Z-критерием.
    funnel - этап воронки, для которого выполняется сравнение групп. '''
    
    print('Группы:', group_1, group_2)
    print('Этап воронки:', funnel, ' - ', funnel_names[funnel - 1])
    
    successes = np.array([users_funnel_cnt(group_1, funnel), users_funnel_cnt(group_2, funnel)])
    trials = np.array([users_group_cnt(group_1), users_group_cnt(group_2)])
    
    # пропорция успехов в первой группе:
    p1 = successes[0]/trials[0]
    
    # пропорция успехов во второй группе:
    p2 = successes[1]/trials[1]
    
    # пропорция успехов в комбинированном датасете:
    p_combined = (successes[0] + successes[1]) / (trials[0] + trials[1])
    
    # разница пропорций в датасетах
    difference = p1 - p2 
    
    print('Доля пользователей этапа воронки к числу пользователей в группе', p1, p2 )

    if diagnostic: 
        # Выводим диагностические данные
        print('p_combined', p_combined)
        print('difference', difference)
    
    if difference == 0:
        p_value = 1    
    else:        
        # считаем статистику в стандартных отклонениях стандартного нормального распределения
        z_value = difference / mth.sqrt(p_combined * (1 - p_combined) * (1/trials[0] + 1/trials[1]))
    
        # задаем стандартное нормальное распределение (среднее 0, стандартное отклонение 1)
        distr = st.norm(0, 1)  
    
        p_value = (1 - distr.cdf(abs(z_value))) * 2    
        
        if diagnostic: 
            # Выводим диагностические данные
            print('z_value', z_value)
            print('distr.cdf(abs(z_value))', distr.cdf(abs(z_value)))    
    
    print('p-значение: ', p_value)
    
    if p_value < alpha:
        print('Отвергаем нулевую гипотезу: между долями есть значимая разница \n')
    else:
        print(
            'Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными \n'
        ) 

Сравнение остальных этапов воронки контрольных групп¶

Сравним остальные этапы воронки контрольных групп с помощью Z-критерия.

Определим гипотезы:
Нулевая - Доли пользователей этапа воронки между контрольными группами A1 A2 равны.
Альтернативная - Доли пользователей этапа воронки между контрольными группами A1 A2 статистически значимо отличаются.

In [64]:
for funnel in range(2,5):
    z_criterion(group_1='A1', group_2='A2', funnel=funnel)
Группы: A1 A2
Этап воронки: 2  -  OffersScreenAppear
Доля пользователей этапа воронки к числу пользователей в группе 0.6207729468599034 0.6054827175208581
p-значение:  0.26698769175859516
Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными 

Группы: A1 A2
Этап воронки: 3  -  CartScreenAppear
Доля пользователей этапа воронки к числу пользователей в группе 0.5096618357487923 0.4922526817640048
p-значение:  0.2182812140633792
Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными 

Группы: A1 A2
Этап воронки: 4  -  PaymentScreenSuccessful
Доля пользователей этапа воронки к числу пользователей в группе 0.4830917874396135 0.4600715137067938
p-значение:  0.10298394982948822
Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными 

Вывод. Сравнение контрольных групп¶

С помощью статистических критериев не удалось обнаружить различия между тестовыми группами. Разделение на группы работает корректно.

Сравнение группы с измененным шрифтом с первой контрольной группой¶

Определим гипотезы:
Нулевая - Доли пользователей этапа воронки между контрольной группой A1 и группой B равны.
Альтернативная - Доли пользователей этапа воронки между контрольной группой A1 и группой B статистически значимо отличаются.

In [65]:
for funnel in range(1,5):
    z_criterion(group_1='A1', group_2='B', funnel=funnel)
Группы: A1 B
Этап воронки: 1  -  MainScreenAppear
Доля пользователей этапа воронки к числу пользователей в группе 0.9863123993558777 0.9830508474576272
p-значение:  0.34705881021236484
Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными 

Группы: A1 B
Этап воронки: 2  -  OffersScreenAppear
Доля пользователей этапа воронки к числу пользователей в группе 0.6207729468599034 0.6034686637761135
p-значение:  0.20836205402738917
Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными 

Группы: A1 B
Этап воронки: 3  -  CartScreenAppear
Доля пользователей этапа воронки к числу пользователей в группе 0.5096618357487923 0.48521876231769806
p-значение:  0.08328412977507749
Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными 

Группы: A1 B
Этап воронки: 4  -  PaymentScreenSuccessful
Доля пользователей этапа воронки к числу пользователей в группе 0.4830917874396135 0.4659046117461569
p-значение:  0.22269358994682764
Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными 

При сравнении группы с измененным шрифтом с первой контрольной группой статистически значимые отличия не обнаружены.

Сравнение группы с измененным шрифтом со второй контрольной группой¶

Определим гипотезы:
Нулевая - Доли пользователей этапа воронки между контрольной группой A2 и группой B равны.
Альтернативная - Доли пользователей этапа воронки между контрольной группой A2 и группой B статистически значимо отличаются.

In [66]:
for funnel in range(1,5):
    z_criterion(group_1='A2', group_2='B', funnel=funnel)
Группы: A2 B
Этап воронки: 1  -  MainScreenAppear
Доля пользователей этапа воронки к числу пользователей в группе 0.9849026618990863 0.9830508474576272
p-значение:  0.6001661582453706
Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными 

Группы: A2 B
Этап воронки: 2  -  OffersScreenAppear
Доля пользователей этапа воронки к числу пользователей в группе 0.6054827175208581 0.6034686637761135
p-значение:  0.8835956656016957
Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными 

Группы: A2 B
Этап воронки: 3  -  CartScreenAppear
Доля пользователей этапа воронки к числу пользователей в группе 0.4922526817640048 0.48521876231769806
p-значение:  0.6169517476996997
Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными 

Группы: A2 B
Этап воронки: 4  -  PaymentScreenSuccessful
Доля пользователей этапа воронки к числу пользователей в группе 0.4600715137067938 0.4659046117461569
p-значение:  0.6775413642906454
Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными 

При сравнении группы с измененным шрифтом со второй контрольной группой статистически значимые отличия не обнаружены.

Сравнение группы с измененным шрифтом с объединенной контрольной группой¶

Определим гипотезы:
Нулевая - Доли пользователей этапа воронки между объединенной контрольной группой A1+A2 и группой B равны.
Альтернативная - Доли пользователей этапа воронки между объединенной контрольной группой A1+A2 и группой B статистически значимо отличаются.

In [67]:
for funnel in range(1,5):
    z_criterion(group_1='A3', group_2='B', funnel=funnel)
Группы: A3 B
Этап воронки: 1  -  MainScreenAppear
Доля пользователей этапа воронки к числу пользователей в группе 0.9856028794241152 0.9830508474576272
p-значение:  0.39298914928006035
Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными 

Группы: A3 B
Этап воронки: 2  -  OffersScreenAppear
Доля пользователей этапа воронки к числу пользователей в группе 0.6130773845230953 0.6034686637761135
p-значение:  0.418998284007599
Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными 

Группы: A3 B
Этап воронки: 3  -  CartScreenAppear
Доля пользователей этапа воронки к числу пользователей в группе 0.5008998200359928 0.48521876231769806
p-значение:  0.19819340844527744
Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными 

Группы: A3 B
Этап воронки: 4  -  PaymentScreenSuccessful
Доля пользователей этапа воронки к числу пользователей в группе 0.471505698860228 0.4659046117461569
p-значение:  0.6452057673098244
Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными 

При сравнении группы с измененным шрифтом с контрольными группами статистически значимые отличия не обнаружены.

Применен критерий статистической значимости с учетом корректировки Бонферрони: 0.003125.
Выполнено 16 проверок.

Полученный уровень значимости:

In [68]:
0.003125 * 16
Out[68]:
0.05

Вывод. Шаг 5. Результаты эксперимента¶

В ходе исследования мы выяснили, как ведут себя пользователи мобильного приложения:

  • Изучили воронку продаж.
  • Узнали, как пользователи доходят до покупки;
  • Подсчитали количество пользователей, которые доходят до покупки, и количество тех, кто «застревает» на предыдущих шагах. Определили на каких именно.
  • Исследовали результаты A/A/B-эксперимента, посвященного выбору лучшего шрифта.

Была выполнена подготовка данных к работе:

  • Выполнили переименование столбцов;
  • Добавили столбец с датой и временем события 'event_dt';
  • Добавили столбец с датой события 'event_date';
  • Удалили 413 дубликатов.
  • Пересечений идентификаторов пользователей между группами теста не выявили.

Всего в собранных данных 243713 уникальных события от 7551 уникального пользователя за период с 25 июля по 7 августа 2019 года.
В среднем на пользователя приходится 32.3 события, максимум 2307 шт.
Количество событий по группам соизмеримо.

В выборке присутствовали данные прошлых периодов. Их мы удалили - 1989 событий (0.8%) и 13 пользователей (0.2%).

Полными можно считать данные с 31 июля 21:00 по 7 августа 2019 года.

После удаления данных количество событий 241724, пользователей 7538.
Всего пользователей в группе A1 - 2484 человека (32.95%), A2 - 2517 (33.39%), B - 2537 (33.66%).
В каждой из трех экспериментальных групп пользователи есть. Количество сопоставимо.

Результаты исследования воронки продаж:
В перечне событий видим:

  • MainScreenAppear (Появится главный экран) - 48.8% от общего числа событий в логах;
  • OffersScreenAppear (Появится экран предложений) - 19.2%;
  • CartScreenAppear (Появится экран корзины) - 17.5%;
  • PaymentScreenSuccessful (Экран оплаты прошел успешно) - 14.0%;
  • Tutorial (Руководство) - 0.4%.

Количество уникальных пользователей, хоть раз совершивших событие, и доля этих пользователей от общего числа:
MainScreenAppear (Появится Главный экран) - 7423 чел., 36.9%;
OffersScreenAppear (Появится экран предложений) - 4597 чел., 22.8%;
CartScreenAppear (Появится экран корзины) - 3736 чел., 18.6%;
PaymentScreenSuccessful (Экран оплаты прошел успешно) - 3540 чел., 17.6%;
Tutorial (Руководство) - 843 чел., 4.2%.

Все события, кроме Tutorial, относятся к воронке продаж. Tutorial - это вспомогательная активность и напрямую к воронке продаж отношения не имеет.

Корректное расположение событий в воронке:

  • MainScreenAppear (Появится Главный экран);
  • OffersScreenAppear (Появится экран предложений);
  • CartScreenAppear (Появится экран корзины);
  • PaymentScreenSuccessful (Экран оплаты прошел успешно).

Конверсия пользователей:

  • с этапа воронки MainScreenAppear на OffersScreenAppear - 61.9%;
  • с этапа OffersScreenAppear в CartScreenAppear - 81.3%;
  • с этапа CartScreenAppear в PaymentScreenSuccessful - 94.8%.

Больше всего пользователей теряется при переходе с первого шага на второй (с главного экрана на экран с предложениями).
Потери составляют 38.1%.

До этапа оплаты доходит 47.7% уникальных пользователей, увидевших главный экран.

Таким образом, обнаружена проблема — провал на первом шаге воронки: от MainScreenAppear к OffersScreenAppear.
Вероятно, нужно лучше прорабатывать механику приложения, чтобы пользователи переходили к OffersScreen.

Для сравнения долей пользователей между группами A/A/B теста на этапах воронки был применен Z-критерий.
Выполнено 16 сравнений независимых выборок.
Приняли критерий статистической значимости с учетом корректировки Бонферрони: 0.003125.

Проведено сравнение двух контрольных групп A1 (246) и A2 (247).

Выявили, что между контрольными группами нет статистически значимого различия долей пользователей, то есть сбор данных корректен.

Этап воронки - p-значение:

  • MainScreenAppear - 0.6756217702005545
  • OffersScreenAppear - 0.26698769175859516
  • CartScreenAppear - 0.2182812140633792
  • PaymentScreenSuccessful - 0.10298394982948822

Затем аналогичным образом провели сравнение первой контрольной группы A1 (246) и группы с измененным шрифтом B (248).
Статистически значимых отличий в доле пользователей на этапах воронки не выявили.

Этап воронки - p-значение:

  • MainScreenAppear - 0.34705881021236484
  • OffersScreenAppear - 0.20836205402738917
  • CartScreenAppear - 0.08328412977507749
  • PaymentScreenSuccessful - 0.22269358994682742

Далее провели сравнение второй контрольной группы A2 (247) и группы с измененным шрифтом B (248).
Статистически значимых отличий в доле пользователей на этапах воронки не выявили.

Этап воронки - p-значение:

  • MainScreenAppear - 0.6001661582453706
  • OffersScreenAppear - 0.8835956656016957
  • CartScreenAppear - 0.6169517476996997
  • PaymentScreenSuccessful - 0.6775413642906454

На финальном этапе провели сравнение объединенной контрольной группы A3 (246+247) с группой с измененным шрифтом B (248).
Статистически значимых отличий в доле пользователей на этапах воронки не выявили.

Этап воронки - p-значение:

  • MainScreenAppear - 0.39298914928006035
  • OffersScreenAppear - 0.418998284007599
  • CartScreenAppear - 0.19819340844527744
  • PaymentScreenSuccessful - 0.6452057673098244

На основе проведенных тестов нельзя сказать, что изменение шрифта положительно скажется на продажах в приложении.
Доработка признана нецелесообразной.